package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.list.dao.GoodsDao;
import com.atguigu.gmall.list.service.ListService;
import com.atguigu.gmall.model.list.*;
import com.atguigu.gmall.model.product.BaseCategoryView;
import com.atguigu.gmall.model.product.BaseTrademark;
import com.atguigu.gmall.model.product.SkuAttrValue;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.client.feign.ProductFeignClient;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author wk
 * @create 2021-09-03 18:26
 */
@Service
public class ListServiceImpl implements ListService {
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate; // 适用于查询
    @Autowired
    private RestHighLevelClient restHighLevelClient; //官方客户端
    @Autowired
    private GoodsDao goodsDao; // 添加 删除 修改
    @Autowired
    private ProductFeignClient productFeignClient;
    @Autowired
    private RedisTemplate redisTemplate;

    // 上架
    @Override
    public void onSale(Long skuId) {
        Goods goods = new Goods();
        // 上架所需要的数据都需要查询出来保存到Goods中
        // 1.库存信息中的数据
        SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
        goods.setId(skuInfo.getId());
        goods.setDefaultImg(skuInfo.getSkuDefaultImg());
        goods.setTitle(skuInfo.getSkuName());
        BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
        goods.setPrice(skuPrice.doubleValue()); // 价格方面使用  BigDecimal
        // 2.创建时间
        Calendar calendar = Calendar.getInstance();
        goods.setCreateTime(calendar.getTime());
        // 3.品牌的相关数据
        BaseTrademark baseTrademark = productFeignClient.getBaseTrademark(skuInfo.getTmId());
        goods.setTmId(baseTrademark.getId());
        goods.setTmName(baseTrademark.getTmName());
        goods.setTmLogoUrl(baseTrademark.getLogoUrl());
        // 4.三级分类id及名字
        BaseCategoryView baseCategoryView = productFeignClient.getBaseCategoryView(skuInfo.getCategory3Id());
        goods.setCategory1Id(baseCategoryView.getCategory1Id());
        goods.setCategory1Name(baseCategoryView.getCategory1Name());
        goods.setCategory2Id(baseCategoryView.getCategory2Id());
        goods.setCategory2Name(baseCategoryView.getCategory2Name());
        goods.setCategory3Id(baseCategoryView.getCategory3Id());
        goods.setCategory3Name(baseCategoryView.getCategory3Name());
        // 5.库存的平台属性集合
        List<SkuAttrValue> skuAttrValueList = productFeignClient.getSkuAttrValue(skuId);
        // stream()流 实现将一个列表中的数据复制到另外一个列表中
        List<SearchAttr> searchAttrList = skuAttrValueList.stream().map((SkuAttrValue) -> {
            SearchAttr searchAttr = new SearchAttr();
            // 填充数据
            // 1.销售属性id
            searchAttr.setAttrId(SkuAttrValue.getBaseAttrInfo().getId());
            // 2.销售属性名
            searchAttr.setAttrName(SkuAttrValue.getBaseAttrInfo().getAttrName());
            // 3.销售属性值
            searchAttr.setAttrValue(SkuAttrValue.getBaseAttrValue().getValueName());
            return searchAttr;
        }).collect(Collectors.toList());
        goods.setAttrs(searchAttrList);
        // 保存
        goodsDao.save(goods);
    }

    // 下架
    @Override
    public void onCancel(Long skuId) {
        // 根据主键删除索引
        goodsDao.deleteById(skuId);
    }

    // 增加热度评分
    @Override
    public void incrHostScore(Long skuId, Integer score) {
        // 没有必要 访问之后立即对热度进行更新 可以先放在缓存中 当达到一定量的时候再进行更新
        String hotScore = "HotScore";
        // 1.先将热度的评分数据暂存在缓存中
        // incrementScore()方法 将增加后的值返回
        // 参数1 表示键
        Double incrementScore = redisTemplate.opsForZSet().incrementScore(hotScore, skuId, score);
        // 2.通过判断达到某个量时再加入到es中
        if (incrementScore % 10 == 0) {
            // 3.先查询
            Optional<Goods> goodsDaoById = goodsDao.findById(skuId);
            Goods goods = goodsDaoById.get();
            // 设置热度分数
            goods.setHotScore(Math.round(incrementScore)); // 四舍五入设置热度分数
            // ES 索引库底层是Lucene 特点是 修改=删除+添加
            // save()方法 的作用相当于修改和添加
            // 其中 如果id存在 那就是修改 否则为添加
            goodsDao.save(goods);
            System.out.println("es更新");
        } else {
            System.out.println("更新缓存");
        }
    }
    // 搜索
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        // 连接es
        // 1.获取搜索请求对象
        SearchRequest searchRequest = builderSearchRequest(searchParam);
        try {
            // 2.开始搜索
            SearchResponse searchResponse = restHighLevelClient.
                    search(searchRequest, RequestOptions.DEFAULT);
            // 3.响应搜索结果
            SearchResponseVo vo = builderSearchResponse(searchResponse);
            // 4.第四步计算总页数
            // 页面需要pageNo 和totalPages
            vo.setPageNo(searchParam.getPageNo());
            vo.setPageSize(searchParam.getPageSize());
            // 总记录数51  每页显示数量5  if(51%5!=0){总页数= 总记录数/每页显示数量+1}else{总页数= 总记录数/每页显示数量}
            // 总页数公式 (总记录数+每页显示数量-1)/每页显示数量  (15+5-1)/5=3
            vo.setTotalPages((vo.getTotal() + searchParam.getPageSize() - 1) / searchParam.getPageSize());
            return vo;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 解析结果
    private SearchResponseVo builderSearchResponse(SearchResponse searchResponse) {
        SearchResponseVo responseVo = new SearchResponseVo();
        //返回值包含 品牌集合  平台属性集合 商品集合 分页相关
        // 1.商品集合 高亮
        // 第一层hits中包含多个字段
        SearchHits hits = searchResponse.getHits();
        // 第二层hits中包含商品集合数据
        SearchHit[] hits1 = hits.getHits();
        // 将数组转换成列表
        List<Goods> goods = Arrays.stream(hits1).map((hit) -> {
            // hit中的_source字段存储商品集合
            String sourceAsString = hit.getSourceAsString();
            Goods stringToGoods = JSONObject.parseObject(sourceAsString, Goods.class);
            // 解析高亮
            // 获取高亮
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (highlightFields != null && highlightFields.size() > 0) {
                // 获取高亮中的字段
                HighlightField title = highlightFields.get("title");
                // 将高亮中字段内容从数组中获取出来并转换为String
                String highLightTitle = title.fragments()[0].toString();
                // 替换 已存在的title
                stringToGoods.setTitle(highLightTitle);
            }
            // 将商品集合的字符串转换为实体类
            return stringToGoods;
        }).collect(Collectors.toList());
        // 填充商品集合
        responseVo.setGoodsList(goods);
        // 2.分页相关
        long totalHits = hits.getTotalHits();
        System.out.println("总记录数" + totalHits);
        // 设置总记录数
        responseVo.setTotal(totalHits);
        // 3.品牌集合解析
        // 根据聚合时设置的别名取值
        ParsedLongTerms tmIdAgg = searchResponse.getAggregations().get("tmIdAgg");
        // List<? extends Terms.Bucket> buckets = tmIdAgg.getBuckets();
        List<SearchResponseTmVo> trademarkList = tmIdAgg.getBuckets().stream().map(bucket -> {
            SearchResponseTmVo responseTmVo = new SearchResponseTmVo();
            // 品牌id
            long tmId = bucket.getKeyAsNumber().longValue();
            responseTmVo.setTmId(tmId);
            // 品牌名字 ParsedStringTerms 是 Aggregation的实现类功能更为强大
            ParsedStringTerms tmNameAgg = bucket.getAggregations().get("tmNameAgg");
            String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
            responseTmVo.setTmName(tmName);
            // 品牌logo
            ParsedStringTerms tmLogoUrlAgg = bucket.getAggregations().get("tmLogoUrlAgg");
            String tmLogoUrl = tmLogoUrlAgg.getBuckets().get(0).getKeyAsString();
            responseTmVo.setTmLogoUrl(tmLogoUrl);
            return responseTmVo;
        }).collect(Collectors.toList());
        responseVo.setTrademarkList(trademarkList);
        // 4.平台属性集合
        //  ParsedNested使用嵌套
        ParsedNested attrsAgg = searchResponse.getAggregations().get("attrsAgg");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg"); // id解析为long类型
        List<SearchResponseAttrVo> attrsList = attrIdAgg.getBuckets().stream().map(bucket -> {
            SearchResponseAttrVo responseAttrVo = new SearchResponseAttrVo();
            // 属性id
            long attrId = bucket.getKeyAsNumber().longValue();
            responseAttrVo.setAttrId(attrId);
            // 属性名称
            ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            responseAttrVo.setAttrName(attrName);
            // 属性值
            ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValueAgg");
            List<String> attrValueList = attrValueAgg.getBuckets().stream()
                    .map(bucket2 -> bucket2.getKeyAsString()).collect(Collectors.toList());
            // 设置平台属性值
            responseAttrVo.setAttrValueList(attrValueList);
            return responseAttrVo;
        }).collect(Collectors.toList());
        // 设置设置平台属性
        responseVo.setAttrsList(attrsList);
        return responseVo;
    }

    // 处理请求
    private SearchRequest builderSearchRequest(SearchParam searchParam) {
        // 1.构建搜索对象 相当与DSL语言中的 GET goods/_search
        SearchRequest searchRequest = new SearchRequest();
        // 2.创建搜索资源构建对象
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 相当于{}
        // 3.查询所有的索引
        // searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        // 创建组合查询对象 可以将查询属性以and的方式组合起来
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 3.1  关键词
        String keyword = searchParam.getKeyword();
        if (!StringUtils.isEmpty(keyword)) {
            boolQueryBuilder.must(QueryBuilders.matchQuery("title", keyword));
        }
        //  3.2品牌 页面中品牌是以id:品牌名的方式展示
        String trademark = searchParam.getTrademark();
        if (!StringUtils.isEmpty(trademark)) {
            // 对页面返回数据进行分割
            String[] split = trademark.split(":");
            // term 精确匹配查询
            boolQueryBuilder.must(QueryBuilders.termQuery("tmId", split[0]));
        }
        // 3.3一二三级分类
        Long category1Id = searchParam.getCategory1Id();
        if (category1Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", category1Id));
        }
        Long category2Id = searchParam.getCategory2Id();
        if (category2Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", category2Id));
        }
        Long category3Id = searchParam.getCategory3Id();
        if (category3Id != null) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", category3Id));
        }
        // 3.7平台属性
        // 平台属性使用数组封装 多个属性值 而页面中平台属性 表现方式是
        // props= 23:4G:运行内存 分别是 平台属性id: 平台属性值: 平台属性名
        String[] props = searchParam.getProps();
        if (props != null && props.length > 0) {
            // 组合对象 用来组合内部的对象
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            // 如果平台属性中存在数据 那么就进行遍历
            for (String prop : props) {
                // 平台属性id: 平台属性值: 平台属性名
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    // 过滤平台属性
                    // attrs内部的多个对象之间是彼此and关系 每一个内部对象都包含属性 attrName是冗余字段可以不用过滤
                    BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery();
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    // 每次遍历之后将attrs内部的每一个对象添加到attrs 组合对象中
                    // 嵌套
                    boolQuery.must(QueryBuilders.nestedQuery("attrs", subBoolQuery, ScoreMode.None));
                }
            }
            // attrs是和一二三级分类是同一等级的 所以使用之前组装分类的组合对象boolQueryBuilder
            // 过滤组合品牌
            boolQueryBuilder.filter(boolQuery);
        }
        // 将组合对象设置进查询当中
        searchSourceBuilder.query(boolQueryBuilder);
        // 3.4 分页 和query平级
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        searchSourceBuilder.from((pageNo - 1) * pageSize);//当前页的起始索引，默认从0开始
        searchSourceBuilder.size(pageSize); // 每页显示数量

        // 3.5排序 默认是按照hotScore进行排序
        // 页面可以按照 价格 创建时间 排序或者综合也就是默认
        // list/index页面 中 价格的排序 是按照 order=2:asc 或者是 order=2:desc
        // 综合的排序 是按照order=1:asc 或者是 order=1:desc 三元运算判断
        String order = searchParam.getOrder();
        if (!StringUtils.isEmpty(order)) {
            // 如果不为空
            String[] split = order.split(":");
            // 或者使用工具类
            // StringUtils.split(order,":");
            // 因为order中的数据是按照1 2 3 等 区分排序的
            String value = "";
            switch (split[0]) {
                case "1":
                    value = "hotScore";
                    break; // 热点
                case "2":
                    value = "price";
                    break; // 价格
                case "3":
                    value = "createTime";
                    break; // 时间 新品排序
            }
            // 使用三元运算 判断数据第二个位置的值
            searchSourceBuilder.sort(value, "asc".equalsIgnoreCase(split[1]) ? SortOrder.ASC : SortOrder.DESC);
        } else {
            // 默认是按照hotScore进行排序
            searchSourceBuilder.sort("hotScore", SortOrder.DESC);
        }
        // 3.6高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        // 往高亮构建对象中设置参数
        // preTags前缀 postTags后缀 field 填写要高亮的字段
        highlightBuilder.preTags("<font style='color:red'>").postTags("</font>").field("title");
        searchSourceBuilder.highlighter(highlightBuilder);
        // 品牌桶聚合 页面中需要 tmId  tmName tmLogUrl
        // 桶聚合相当于SQL中的group by分组
        // 对tmId进行聚合 只要tmId不重复 就可以保证tmName不重复
        // term()设置聚合后结果的别名 方便解析时取值 field()设置要聚合的域 size()还可以设置长度
        searchSourceBuilder
                .aggregation(AggregationBuilders.terms("tmIdAgg").field("tmId")
                        .subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"))
                        .subAggregation(AggregationBuilders.terms("tmLogoUrlAgg").field("tmLogoUrl"))
                );
        // 平台属性桶聚合 桶聚合只能聚合一个字段 其余字段当作子节点跟随聚合
        // 要对attr中的子属性进行聚合
        searchSourceBuilder
                .aggregation(AggregationBuilders.nested("attrsAgg", "attrs")
                        // 对id进行聚合
                        .subAggregation(AggregationBuilders.terms("attrIdAgg").field("attrs.attrId")
                                .subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName"))
                                .subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue"))
                        )
                );
        // 4.将搜索资源构建对象设置给搜索对象
        searchRequest.source(searchSourceBuilder);
        System.out.println(searchSourceBuilder);
        // 5.设置查询的索引库的名字
        searchRequest.indices("goods");
        return searchRequest;
    }
}
